/*
 *
 */
#include <net-snmp/net-snmp-config.h>
#include <net-snmp/net-snmp-includes.h>
#include <net-snmp/agent/net-snmp-agent-includes.h>
#include <net-snmp/library/snmp_assert.h>

#include "snmpstats_globals.h"
#include "UserStateTable.h"
#include "../../lib/srdb1/db.h"
#include "../../dprint.h"
#include "../usrloc/ul_callback.h"
#include "../../timer.h"
#include "../../mem/shm_mem.h"

/*
 *  DB Strings
 */
str table_usr_privateinfo = str_init(TABLE_USR_PRIVATEINFO);
str table_location        = str_init(TABLE_LOCATION);
str column_username       = str_init(COLUMN_USERNAME);

UserStateTable_context *UserStateTable_addRow(int index, int state);

void                    UserStateTable_addOrUpdateRow(int index, int state);

void UserStateTable_resetContent(void);

void UserStateTable_updateContent(void);

static netsnmp_handler_registration *my_handler = NULL;

static netsnmp_table_array_callbacks cb;

const oid       UserStateTable_oid[] = { UserStateTable_TABLE_OID };
const size_t    UserStateTable_oid_len = OID_LENGTH(UserStateTable_oid);


#ifdef UserStateTable_CUSTOM_SORT
/*
 * keep binary tree to find context by name
 */
static int      UserStateTable_cmp(const void *lhs, const void *rhs);

/*
 * compare two context pointers here. Return -1 if lhs < rhs,
 * 0 if lhs == rhs, and 1 if lhs > rhs.
 */
static int
UserStateTable_cmp(const void *lhs, const void *rhs)
{
    UserStateTable_context *context_l = (UserStateTable_context *) lhs;
    UserStateTable_context *context_r = (UserStateTable_context *) rhs;

    /*
     * check primary key, then secondary. Add your own code if
     * there are more than 2 keys
     */
    int             rc;

    /*
     * TODO: implement compare. Remove this ifdef code and
     * add your own code here.
     */
#ifdef TABLE_CONTAINER_TODO
    snmp_log(LOG_ERR,
             "UserStateTable_compare not implemented! Container order undefined\n");
    return 0;
#endif

    /*
     * EXAMPLE (assuming you want to sort on a name):
     *
     * rc = strcmp( context_l->xxName, context_r->xxName );
     *
     * if(rc)
     *   return rc;
     *
     * TODO: fix secondary keys (or delete if there are none)
     *
     * if(context_l->yy < context_r->yy)
     *   return -1;
     *
     * return (context_l->yy == context_r->yy) ? 0 : 1;
     */
}

/*
 * search tree
 */
/** TODO: set additional indexes as parameters */
UserStateTable_context *
UserStateTable_get(const char *name, int len)
{
    UserStateTable_context tmp;

    /** we should have a custom container */
    netsnmp_assert(cb.container->next != NULL);

    /*
     * TODO: implement compare. Remove this ifdef code and
     * add your own code here.
     */
#ifdef TABLE_CONTAINER_TODO
    snmp_log(LOG_ERR, "UserStateTable_get not implemented!\n");
    return NULL;
#endif

    /*
     * EXAMPLE:
     *
     * if(len > sizeof(tmp.xxName))
     *   return NULL;
     *
     * strncpy( tmp.xxName, name, sizeof(tmp.xxName) );
     * tmp.xxName_len = len;
     *
     * return CONTAINER_FIND(cb.container->next, &tmp);
     */
}
#endif


/*
 * Initializes the UserStateTable module
 */
void
init_UserStateTable(void)
{
    initialize_table_UserStateTable();
    UserStateTable_resetContent();
    UserStateTable_updateContent();
}



/*
 * Initialize the UserStateTable table by defining its contents and how it's structured
 */
void
initialize_table_UserStateTable(void)
{
    netsnmp_table_registration_info *table_info;

    if (my_handler) {
        snmp_log(LOG_ERR,
                 "initialize_table_UserStateTable_handler called again\n");
        return;
    }

    memset(&cb, 0x00, sizeof(cb));

    /** create the table structure itself */
    table_info = SNMP_MALLOC_TYPEDEF(netsnmp_table_registration_info);

    my_handler = netsnmp_create_handler_registration("UserStateTable",
                                                     netsnmp_table_array_helper_handler,
                                                     UserStateTable_oid,
                                                     UserStateTable_oid_len,
                                                     HANDLER_CAN_RONLY);

    if (!my_handler || !table_info) {
        snmp_log(LOG_ERR, "malloc failed in "
                 "initialize_table_UserStateTable_handler\n");
        return; /** mallocs failed */
    }

    /***************************************************
     * Setting up the table's definition
     */
    /*
     * TODO: add any external indexes here.
     */
    /*
     * internal indexes
     */
        /** index: UserId */
    netsnmp_table_helper_add_index(table_info, ASN_UNSIGNED);

    table_info->min_column = UserStateTable_COL_MIN;
    table_info->max_column = UserStateTable_COL_MAX;

    /***************************************************
     * registering the table with the master agent
     */
    cb.get_value = UserStateTable_get_value;
    cb.container = netsnmp_container_find("UserStateTable_primary:"
                                          "UserStateTable:"
                                          "table_container");
    #ifdef UserStateTable_CUSTOM_SORT
    netsnmp_container_add_index(cb.container,
                                netsnmp_container_find
                                ("UserStateTable_custom:" "UserStateTable:"
                                 "table_container"));
    cb.container->next->compare = UserStateTable_cmp;
    #endif

    DEBUGMSGTL(("initialize_table_UserStateTable",
                "Registering table UserStateTable " "as a table array\n"));
    netsnmp_table_container_register(my_handler, table_info, &cb,
                                     cb.container, 1);
}

static time_t last_request_timestamp = 0;

/*
 * UserStateTable_get_value
 *
 * This routine is called for get requests to copy the data
 * from the context to the varbind for the request. If the
 * context has been properly maintained, you don't need to
 * change in code in this fuction.
 *
 * Provoked by snmp get request
 */
int
UserStateTable_get_value(netsnmp_request_info *request,
                         netsnmp_index * item,
                         netsnmp_table_request_info *table_info)
{
    time_t now;
    time(&now);

    // INFO("now=%ld", now);
    // INFO("last=%ld", last_request_timestamp);
    if (now - last_request_timestamp > UserStateTable_UPDATE_INTERVAL) {
        LM_INFO("Reloading UserStateTable");
        time(&last_request_timestamp);
        UserStateTable_resetContent();
        UserStateTable_updateContent();
    } else {
        LM_INFO("Ignore reloading UserStateTable");
    }


    netsnmp_variable_list *var = request->requestvb;
    UserStateTable_context *context = (UserStateTable_context *) item;

    switch (table_info->colnum) {

    case COLUMN_USERID:
            /** UNSIGNED32 = ASN_UNSIGNED */
        snmp_set_var_typed_value(var, ASN_UNSIGNED,
                                 (char *) &context->UserId,
                                 sizeof(context->UserId));
        break;

    case COLUMN_USERSTATE:
            /** INTEGER = ASN_INTEGER */
        snmp_set_var_typed_value(var, ASN_INTEGER,
                                 (char *) &context->UserState,
                                 sizeof(context->UserState));
        break;

    default:/** We shouldn't get here */
        snmp_log(LOG_ERR, "unknown column in "
                 "UserStateTable_get_value\n");
        return SNMP_ERR_GENERR;
    }
    return SNMP_ERR_NOERROR;
}

/*
 * UserStateTable_get_by_idx
 */
const UserStateTable_context *
UserStateTable_get_by_idx(netsnmp_index * hdr)
{
    return (const UserStateTable_context *)
        CONTAINER_FIND(cb.container, hdr);
}

/*************************************************
 * Container manipulation
 *************************************************/
/*
 * add a row in snmp container
 */
UserStateTable_context * UserStateTable_addRow(int index, int state) {

    UserStateTable_context *theRow;
    theRow = shm_malloc(sizeof(UserStateTable_context));
    if (theRow == NULL) {
        LM_ERR("failed to create a row for UserStateTable\n");
        return NULL;
    }

    oid *OIDIndex;
    OIDIndex = shm_malloc(sizeof(oid));
    if (OIDIndex == NULL) {
        shm_free(theRow);
        LM_ERR("failed to allocate space for a OID for UserStateTable\n");
        return NULL;
    }
    OIDIndex[0] = index;
    theRow->index.len  = 1;
    theRow->index.oids = OIDIndex;

    theRow->UserId = (unsigned long)index;
    theRow->UserState = (unsigned long)state;
    // LM_INFO("ID=%lu,STATE=%lu", theRow->UserId, theRow->UserState);

    CONTAINER_INSERT(cb.container, theRow);
    return theRow;
}

/*
 * Update the snmp container, if the row does not exist, add it into container.
 */
void UserStateTable_addOrUpdateRow(int index, int state) {

    UserStateTable_context *theRow;
    netsnmp_index indexToUpdate;
    oid indexToUpdateOID;

    indexToUpdateOID   = index;
    indexToUpdate.oids = &indexToUpdateOID;
    indexToUpdate.len  = 1;

    if(cb.container == NULL)
        LM_INFO("No Container");
    else {
        theRow = CONTAINER_FIND(cb.container, &indexToUpdate);
        if (theRow != NULL) {
            theRow->UserState = state;
            // LM_INFO("User[%d] is updated.", index);
        } else {
            // LM_INFO("No row found. Add new row ID=%d, STATE=%d", index, state);
            UserStateTable_addRow(index, state);
        }
    }
}



/*
 * This function reads from database all the subsribers into the snmp container
 * assuming they are currently offline.
 */
void UserStateTable_resetContent() {

    str db_url = str_init(DEFAULT_DB_URL);

    db_func_t  db_functions;

    // results pointer
    db1_res_t* db_result = NULL;

    /* Bind the database module */
    if (db_bind_mod(&db_url, &db_functions)) {
        LM_ERR("failed to bind database module\n");
        return;
    }

    /* Initialize connection to DB */
    db1_con_t *db_connection = db_functions.init(&db_url);
    if (!db_connection) {
        LM_ERR("Failed to connect database\n");
        return;
    }

    /* use User table */
    if (db_functions.use_table(db_connection, &table_usr_privateinfo) < 0) {
        LM_ERR("Error occured in use_table\n");
        return;
    }

    // column names construction
    db_key_t result_columns[QUERY_COUNT] = { &column_username };

    // finally ready for the query
    if(0 != db_functions.query(db_connection,
                               NULL,            // key
                               NULL,            // operator
                               NULL,            // value for WHERE part
                               result_columns,  // columns in which we are interested.
                               0,               // number of key-value pair above
                               1 ,              // number of columns above
                               NULL,            // order
                               &db_result       // results
                              )
      ) {
        LM_ERR("Failed to query the database\n");
        db_functions.close(db_connection);
        return;
    }

    /* Results Handling */
    // LM_INFO("Query from [%s] -> %d row(s) fetched.", table_usr_privateinfo.s, RES_ROW_N(db_result));

    if (RES_ROW_N(db_result)<=0 || RES_ROWS(db_result)[0].values[0].nul != 0) {
        if (db_result != NULL && db_functions.free_result(db_connection, db_result) < 0)
            LM_WARN("Failed to free the result\n");
        db_functions.close(db_connection);
        return;
    }

    int i = 0;
    for(i = 0; i < RES_ROW_N(db_result); i++) {
        int userId = atoi( (char*)RES_ROWS(db_result)[i].values[0].val.string_val );
        UserStateTable_addOrUpdateRow(userId, 0);

        /* I know the result type is DB1_STRING, so no need to determine result's data type
        if(RES_ROWS(db_result)[i].values[0].type == DB1_STRING)
            LM_INFO("first value found is STRING [%s]\n",
                    (char*)RES_ROWS(db_result)[i].values[0].val.string_val);
        else if(RES_ROWS(db_result)[i].values[0].type == DB1_STR)
            LM_INFO("first value found is STR [%.*s]\n",
                    RES_ROWS(db_result)[i].values[0].val.str_val.len,
                    (char*)RES_ROWS(db_result)[i].values[0].val.str_val.s);
        else
            LM_INFO("first value found has an unexpected type [%d]\n",
                    RES_ROWS(db_result)[i].values[0].type);
        */
    }
    /* Free the result */
    db_functions.free_result(db_connection, db_result);
    db_functions.close(db_connection);
}

/*
 * This function reads from location TABLE the curent online users and
 * update the snmp container.
 */
void UserStateTable_updateContent() {

    LM_INFO("Updating UserStateTable from DB");
    str db_url = str_init(DEFAULT_DB_URL);
    db_func_t  db_functions;
    db1_res_t* db_result = NULL; // results pointer

    /* Bind the database module */
    if (db_bind_mod(&db_url, &db_functions)) {
        LM_ERR("failed to bind database module\n");
        return;
    }
    /* Initialize connection to DB */
    db1_con_t *db_connection = db_functions.init(&db_url);
    if (!db_connection) {
        LM_ERR("failed to connect database\n");
        return;
    }
    /* User table */
    if (db_functions.use_table(db_connection, &table_location) < 0) {
        LM_ERR("Error occured in use_table\n");
        return;
    }
    // column names construction
    db_key_t result_columns[QUERY_COUNT] = { &column_username };

    // finally ready for the query
    if(0 != db_functions.query(db_connection,
                               NULL,             // key
                               NULL,             // operator
                               NULL,             // value
                               result_columns,   // columns in which we are interested.
                               0,                // number of key-value pair
                               1,                // number of column
                               NULL,             // order
                               &db_result        // results
                              )
      ) {
        LM_ERR("failed to query database\n");
        db_functions.close(db_connection);
        return;
    }

    /* Handles result */
    LM_INFO("Query online user from [%s] -> %d row(s) fetched.", table_location.s, RES_ROW_N(db_result));

    if (RES_ROW_N(db_result)<=0 || RES_ROWS(db_result)[0].values[0].nul != 0) {
        LM_INFO("no value found\n");
        if (db_result != NULL && db_functions.free_result(db_connection, db_result) < 0)
            LM_WARN("Failed to free db result\n");
        db_functions.close(db_connection);
        return;
    }

    int i = 0;
    for(i = 0; i <RES_ROW_N(db_result); i++) {
        int userId = atoi( (char*)RES_ROWS(db_result)[i].values[0].val.string_val );
        UserStateTable_addOrUpdateRow(userId, 1);
    }

    /* Free the result */
    db_functions.free_result(db_connection, db_result);
    db_functions.close(db_connection);
}

